Inline Teardown
The book has now been published and the content of this chapter has likely changed substanstially.How do we tear down the Test Fixture?
We include teardown logic at the end of the Test Method immediately after the result verification.
Sketch Inline Teardown embedded from Inline Teardown.gif
A large part of making tests repeatable and robust is ensuring that the test fixture is torn down after each test. Leftover objects and database records, open files and connections can at best cause performance degradations and at worst cause tests to fail or systems to crash. While some of these resources may be cleaned up automatically by garbage collection, others may be left hanging if they are not torn down explicitly.
As a minimum, we should write Inline Teardown code that cleans up resources leftover after our test.
How It Works
As we write a test, we mentally keep track of all the objects the test is creating that will not be cleaned up automatically. After writing the code to exercise the system under test (SUT) and verify the outcome, we add logic to the end of the Test Method (page X) to destroy any objects that will not be cleaned up automatically by the garbage collector. We use the relevant language feature to ensure that the tear down code gets run regardless of the outcome of the test.
When To Use It
We should use some form of tear down logic whenever we have resources that will not be freed automatically after the Test Method is run; we can use Inline Teardown when each test has different objects to clean up. We may discover that objects need to be cleaned up because we have Unrepeatable Tests (see Erratic Test on page X) or Slow Tests (page X) caused by the accumulation of detritus from many test runs.
Unlike fixture setup, the tear down logic is not important from the perspective of Tests as Documentation (see Goals of Test Automation on page X). Use of any form of tear down logic is a possible contributor to High Test Maintenance Cost (page X) and should be avoided if at all possible. Therefore the only real benefit of including the tear down logic inline is that it may make it easier to maintain the tear down logic but this is a pretty slim benefit. It is almost always better to strive for Automated Teardown (page X) or to use Implicit Teardown (page X) if we are using Testcase Class per Fixture (page X) (where all the tests in a Testcase Class (page X) have the same test fixture.)
We can also use Inline Teardown as a stepping stone to Implicit Teardown following the principle of "the simplest thing that could possibly work." First, we learn how to do Inline Teardown for each Test Method and then we extract the common logic from those tests into the tearDown method. We should not use Inline Teardown if the objects created by the test are subject to automated memory management; we should use Garbage-Collected Teardown (page X) instead because it is much less error-prone and keeps the tests easier to understand and maintain.
Implementation Notes
The primary consideration in Inline Teardown is ensuring that the tear down code actually runs even when the test is failed by an Assertion Method (page X) or ends in an error in either the SUT or the Test Method. A secondary consideration is ensuring that the tear down code does not introduce further errors.
The key to doing Inline Teardown correctly is to use language level constructs to ensure that the code gets run. Most modern languages have some sort of error/exception handling construct to attempt the execution of a block of code with the guarantee that a second block of code will be run regardless of how the first block terminates. In Java, it takes the form of a try block with its associated finally block.
Variation: Teardown Guard Clause
To protect against a failure caused by trying to tear down a resource that doesn't exist, we can put a "guard clause" around the logic to avoid trying to tear it down when the resource doesn't exist. This reduces the likelihood of a test error caused by the tear down logic.
Variation: Delegated Teardown
We can move much of the teardown logic out of the Test Method by calling a Test Utility Method (page X). This does reduce the amount of tear down logic cluttering the test but we still need to have the error handling construct around at least the assertions and the exercising of the SUT to ensure that it gets called. Using Implicit Teardown is almost always a better solution.
Variation: Naive Inline Teardown
This is what we have when we forget to put the equivalent of a try/finally around our test logic to ensure that our tear down logic always gets run. This leads to Resource Leakage (see Erratic Test) which may lead to Erratic Tests.
Motivating Example
The test below creates a persistent object (the airport) as part of the fixture. Since the object is stored in a database, it is not subject to Garbage-Collected Teardown and must be explicitly destroyed. If tear down logic is not included in the test then each time the test is run it will create another object in the database. This may lead to Unrepeatable Tests unless the test uses Distinct Generated Values (see Generated Value on page X) to ensure that the created objects have do not violate any unique key constraints.
public void testGetFlightsByOriginAirport_NoFlights_ntd() throws Exception { // Fixture setup BigDecimal outboundAirport = createTestAirport("1OF"); // Exercise System List flightsAtDestination1 = facade.getFlightsByOriginAirport(outboundAirport); // Verify Outcome assertEquals(0,flightsAtDestination1.size()); } Example NoJavaTeardown embedded from java/com/clrstream/ex6/services/test/InlineTeardownExampleTest.java
Example: Naive Inline Teardown
In this naive solution to this problem, a line has been added after the assertion to destroy the airport created in the fixture setup.
public void testGetFlightsByOriginAirport_NoFlights() throws Exception { // Fixture setup BigDecimal outboundAirport = createTestAirport("1OF"); // Exercise System List flightsAtDestination1 = facade.getFlightsByOriginAirport(outboundAirport); // Verify Outcome assertEquals(0,flightsAtDestination1.size()); facade.removeAirport(outboundAirport); } Example NaiveJavaTeardown embedded from java/com/clrstream/ex6/services/test/InlineTeardownExampleTest.java
Unfortunately, this isn't really enough because the tear down logic won't be exercised if the SUT encounters an error or if the assertions fail. We could try moving the fixture cleanup before the assertions but this still wouldn't address the issue with errors occurring inside the SUT so we need a more general solution.
Refactoring Notes
We either we need to introduce an error handling construct around the exercising of the SUT and the assertions or we need to move the tear down code into the tearDown method. Either way, we need to ensure that all the tear down code is run even if some of it fails. This usually involves the judicious use of try/finally control structures around each step of the tear down process.
Example: Inline Teardown
In this Java example, we have introduced a try/finally block around the exercise SUT and result verification phases of the test to ensure that our tear down code gets run.
public void testGetFlightsByOriginAirport_NoFlights_td() throws Exception { // Fixture setup BigDecimal outboundAirport = createTestAirport("1OF"); try { // Exercise System List flightsAtDestination1 = facade.getFlightsByOriginAirport(outboundAirport); // Verify Outcome assertEquals(0,flightsAtDestination1.size()); } finally { facade.removeAirport(outboundAirport); } } Example GuaranteedJavaTeardown embedded from java/com/clrstream/ex6/services/test/InlineTeardownExampleTest.java
Note that the exercising of the SUT and the assertions are both in the try block and the tear down logic is in the finally block. This is crucial to making Inline Teardown work properly. We should not include a catch block unless this is an Expected Exception Test (see Test Method).
Example: Teardown Guard Clause
Here, we've added a Teardown Guard Clause to the tear down code to ensure it isn't run if the airport doesn't exist:
public void testGetFlightsByOriginAirport_NoFlights_TDGC() throws Exception { // Fixture setup BigDecimal outboundAirport = createTestAirport("1OF"); try { // Exercise System List flightsAtDestination1 = facade.getFlightsByOriginAirport(outboundAirport); // Verify Outcome assertEquals(0,flightsAtDestination1.size()); } finally { if (outboundAirport!=null) { facade.removeAirport(outboundAirport); } } } Example JavaTeardownGuardClause embedded from java/com/clrstream/ex6/services/test/InlineTeardownExampleTest.java
Example: Multi-resource Inline Teardown (Java)
If there are multiple resources that need to be cleaned up in the same test, we must ensure that all the tear down code runs even if some of the tear down statements have errors. This can be done by nesting each subsequent tear down step inside another block of guaranteed code as in this Java example:
public void testGetFlightsByOrigin_NoInboundFlight_SMRTD() throws Exception { // Fixture setup BigDecimal outboundAirport = createTestAirport("1OF"); BigDecimal inboundAirport = null; FlightDto expFlightDto = null; try { inboundAirport = createTestAirport("1IF"); expFlightDto = createTestFlight(outboundAirport, inboundAirport); // Exercise System List flightsAtDestination1 = facade.getFlightsByOriginAirport(inboundAirport); // Verify Outcome assertEquals(0,flightsAtDestination1.size()); } finally { try { facade.removeFlight(expFlightDto.getFlightNumber()); } finally { try { facade.removeAirport(inboundAirport); } finally { facade.removeAirport(outboundAirport); } } } } Example SafeMultiResourceGuaranteedTeardown embedded from java/com/clrstream/ex6/services/test/InlineTeardownExampleTest.java
This gets very messy in a hurry if we have more than a couple of resources to clean up. At that point, it makes more sense to put them into an array or list and iterate over it at which point we are half-way to implementing Automated Teardown.
Example: Delegated Teardown
We can also delegate the tear down from within the Test Method if we don't feel we can come up with a completely generic way to clean up that will work for all tests.
public void testGetFlightsByOrigin_NoInboundFlight_DTD() throws Exception { // Fixture setup BigDecimal outboundAirport = createTestAirport("1OF"); BigDecimal inboundAirport = null; FlightDto expectedFlightDto = null; try { inboundAirport = createTestAirport("1IF"); expectedFlightDto = createTestFlight( outboundAirport, inboundAirport); // Exercise System List flightsAtDestination1 = facade.getFlightsByOriginAirport(inboundAirport); // Verify Outcome assertEquals(0,flightsAtDestination1.size()); } finally { teardownFlightAndAirports( outboundAirport, inboundAirport, expectedFlightDto); } } private void teardownFlightAndAirports( BigDecimal firstAirport, BigDecimal secondAirport, FlightDto flightDto) throws FlightBookingException { try { facade.removeFlight( flightDto.getFlightNumber() ); } finally { try { facade.removeAirport(secondAirport); } finally { facade.removeAirport(firstAirport); } } } Example DelegatedTeardown embedded from java/com/clrstream/ex6/services/test/InlineTeardownExampleTest.java
The optimizers amongst us will notice that the two flight numbers are actually available as attributes of the flightDto. The paranoid will counter that since the teardownFlightAndAirports method could be called before the flightDto is constructed, we cannot count on using it to access the Airports. Hence we must pass the Airports in individually. The need to think this way is why a generic Automated Teardown is so attractive!
Copyright © 2003-2008 Gerard Meszaros all rights reserved